#!/usr/bin/env ruby

##
##  Rob's JMock 1 annihilation script.
##
##  Parses the tests to be de-JMock1-ified, attempts to replace all the 
##  JUnit 3/JMock 1 rubbish with equivalent JUnit 4/Moxie calls.
##
##  DISCLAIMER: This hastily written script should do 90% of the 
##  donkey work of migrating your tests to Moxie and JUnit 4 - 
##  but it can't catch everything.  This script may choke when 
##  trying to parse some files, makes wild guesses in places, 
##  and won't always result in compiling/correct code.
##  Be sure to manually check the new code against the original
##  before you commit!
##
##  usage: either run this script directly...
##
##      junmock.rb [list of Java unit test files]
##
##  ...or see a usage example after the __END__ of this file.
##


require 'set'


module UnJMock1

#
# Generate regexp pattern to match a method body - i.e. a balanced pair of braces that may itself contain up to 10 pairs of braces.
#

BRACES_LEFT  = '[^\{\}]*(?:\{'   
BRACES_RIGHT = '\}[^\{\}]*)*'
BRACES = '\{('+(BRACES_LEFT*10)+'.*?'+(BRACES_RIGHT*10)+')\}'

#
# same concept for balanced parentheses nested up to 10 deep
#
PARENS_LEFT  = '[^\(\)]*(?:\('
PARENS_RIGHT = '\)[^\(\)]*)*'
PARENS_CONTENT = (PARENS_LEFT*10)+'.*?'+(PARENS_RIGHT*10)

#
# same concept for balanced angle brackets nested up to 10 deep
#
ANGLES_LEFT  = '[^<>]*(?:<'
ANGLES_RIGHT = '>[^<>]*)*'
ANGLES = '<'+(ANGLES_LEFT*10)+'.*?'+(ANGLES_RIGHT*10)+'>'


#
# Regexp used to scan Java method invocation parameters lists.
#
PARAM_SCAN_REGEXP = /(?:[^\(\),]|\(#{PARENS_CONTENT}\))+/o

#
# Regexp to parse Java methods declarations.
#
METHODS_REGEXP = /
  (?:\b(
  ((?:(?:final|static|public|private|protected)\s+)*)       # access modifier
  (?:#{ANGLES}\s+)?                                         # method type parameter
  (?:((?:\w+\.)*\w+(?:\[\])?)\s+)?                          # return type (optional - may be a constructor)
  (\w+)\s*                                                  # method name
  \((#{PARENS_CONTENT})\)                                   # parameters
  (?:\s+throws\s+(?:[\w\.]+,\s*)*(?:[\w\.]+))?              # throws clause
  \s*\{([^\{\}]*(?:[^\{\}]*|
  #{BRACES}                                                 # method body
  )*)\})
  )/mxo

#
# Regexp to pick up JMock 1 expectation calls.
#
MOCK_EXPECTATION_CALL = /
  \b(\w[^\s\.]+)\.                                          # mock variable
  (?:(stubs)\(\)|expects\((\w+)\((#{PARENS_CONTENT})\)\))   # stubs() or expects(howOften())
  [\s\r\n]*\.[\s\r\n]*
  method\(\"(\w+)\"\)                                       # method name
  (?:
    [\s\r\n]*\.[\s\r\n]*
    (with\w*)\((#{PARENS_CONTENT})\)                        # withNoArguments() or with(args...)
  )?
  (?:[\s\r\n]*\.[\s\r\n]*after\(\"([^"]+)\"\))?             # after("someCall") - used in sequences
  (?:
    [\s\r\n]*\.[\s\r\n]*
    (?:isVoid\(\)|will\((#{PARENS_CONTENT})\))
  )?
  (?:[\s\r\n]*\.[\s\r\n]*id\(\"([^"]+)\"\))?                # id("someCall") - used in sequences
  [\s\r\n]*;/mxo

#
# Regexp for JMock 1 "will" clauses.
#
WILL_REGEXP = /(?:(throw\w*|return\w*|new\s+(?:Throw|Return)Stub)\s*\((#{PARENS_CONTENT})\)|onConsecutiveCalls\s*\((#{PARENS_CONTENT})\))\s*(?:,|$)/mo

#
# Regexp for JUnit 3 assertions.
#
ASSERT_REGEXP = /((?:;|\{|$)[\s\r\n]*)\b((?:assert(?:Equals|True|False|(?:Not)?(?:Null|Same))|fail)\s*\(#{PARENS_CONTENT}\)\s*(?=;))/mo


#
# Iterate over parts of a Java source file.
#
def UnJMock1.for_each_method(source)
  source.gsub(METHODS_REGEXP) do
    all_match, method, access, return_type, name, params, body =  $&, $1, $2, $3, $4, $5, $6
    yield(all_match, nil, method, access, return_type, name, params, body) || all_match
  end
end

#
# utility method - add an import if it doesn't already exist
#
def UnJMock1.add_import(source, import)
  last_import = source.scan(/\bimport\s+(?:static\s+)?[\w\.]+\s*;/).last
  import_rx = last_import ? Regexp.compile(Regexp.quote(last_import)) : /^package\s+[\w\.]+\s*;/
  source = source.sub(import_rx) {"#{$&}\nimport #{import};"} if source !~ /\bimport\s+#{Regexp.quote(import)}\s*;/
  source
end

#
# translate JMock 1-style parameter constraints to Moxie style
#
def UnJMock1.translate_constraints(param_string)
  param_string = param_string.sub(/^[\s\r\n]*new[\s\r\n]+(?:org\.jmock\.core\.)?Constraint\s*\[\][\s\r\n]*#{BRACES}[\s\r\n]*$/mo, '\1').strip
  param_string.gsub!(PARAM_SCAN_REGEXP) do 
    all_match = $&
    param = all_match.strip
    param = param[0,param.length-1].strip if param[-1,1] == ','
    result = param
    case param 
      when /^(and|or|not)\((#{PARENS_CONTENT})\)$/mo
      	result = "Moxie.#{$1}\(#{translate_constraints($2)}\)"
      when /^eq\((#{PARENS_CONTENT})\)$/mo
        case $1
          when 'null'
            result = 'Moxie.isNull()'
          when 'false', '0'
            result = "Moxie.eq(#{$1})"
          else
            result = $1
        end
      when /^(same|isA|startsWith|endsWith|anything)\((#{PARENS_CONTENT})\)$/mo
        result = "Moxie.#{$1}(#{$2})"
      when /^(?:stringC|c)ontains\((#{PARENS_CONTENT})\)$/mo
        result = "Moxie.hasSubstring(#{$1})"
      when /^ANYTHING$/
        result = "Moxie.anything()"
      when /^NULL$/
        result = "Moxie.isNull()"
      when /^NOT_NULL$/
        result = "Moxie.notNull()"
    end
    all_match.sub(Regexp.compile(Regexp.quote(param)), result)
  end
end

#
# translate a JMock 1 style "will" clause
#
def UnJMock1.translate_will(will)
  will = '' if will.nil?
  will_clauses = will.scan(WILL_REGEXP).collect do |x|
    will_method, will_params, consecutive_params = x
    q = $&
    will_params = $1 if will_params =~ /^\((.*)\)$/m
    if consecutive_params
      translate_will(consecutive_params)
    elsif will_method =~ /\bthrow/i
      ".andThrow(#{will_params})"
    elsif will_method =~ /\breturn/i
      ".andReturn(#{will_params})"
    else
      q  # panic and create a big syntax error...
    end
  end
  will_clauses.flatten!
  return will_clauses.empty? ? [''] : will_clauses
end

#
# translate a JMock 1 style mock expectation to Moxie syntax
#
def UnJMock1.translate_mock_expectation(groups, imports, variable, stubs, expect, expect_params, method_name, with, with_params, after, will, id)
  imports << 'moxie.Moxie'

  # param string - detect and replace constraints
  param_string = translate_constraints(with_params || '')
  if with == 'withAnyArguments'
    # HACKTAG: no easy way of knowing how many params, so just put in one anything() and let a human sort things out if that doesn't work
    param_string = 'Moxie.anything()'
  end
  
  times = ''
  if stubs =~ /\w/
    expectation = "Moxie.stub(#{variable})"
  else
    expectation = "Moxie.expect(#{variable})"
    if expect == 'exactly'
      times = ".times(#{expect_params})"
    else
      times = ".#{expect}(#{expect_params})"
    end
  end

  will_clauses = translate_will(will)
  will_returns = will_clauses.grep(/^\.andReturn\((.*)\)$/){$1}
  will_clauses = [".andConsecutivelyReturn(#{will_returns.join(', ')})"] if will_clauses.length > 1 && will_clauses.length == will_returns.length
  will = will_clauses.join('')
  on = will_clauses.empty? ? '.will()' : '.on()'
  
  invocation = ".#{method_name}(#{param_string});"

  group_decl = group = ''
  if after =~ /\w/ || id =~ /\w/
    group = groups["#{variable}/#{after}"] || groups["#{variable}/#{id}"]
    if group.nil?
      imports << 'moxie.Group'
      imports << 'moxie.MoxieOptions'
      group_name = "group" + groups.size.to_s
      group_decl = "Group #{group_name} = Moxie.group(\"#{group_name}\", MoxieOptions.ORDERED);\n\t\t"
      group = ".inGroup(#{group_name})"
    end
    groups["#{variable}/#{after}"] = group if after =~ /\w/ && groups["#{variable}/#{after}"].nil?
    groups["#{variable}/#{id}"] = group if id =~ /\w/ && groups["#{variable}/#{id}"].nil?
  end
  
  group_decl + expectation+group+times+will+on+invocation
end


def UnJMock1.convert(source)
  imports = Set.new
  resubs, resub_counter = {}, 0

  # Don't try to replace things in comment blocks.
  source.gsub!(/(\/\/\s*)(\S.*?)(\s*$)/) {
    name = "__SHORT_COMMENT_#{resub_counter}__"
    resub_counter = resub_counter + 1
    resubs[name] = $2
    $1+name+$3
  }
  source.gsub!(/\/\*(.*?)\*\//m) {
    name = "__LONG_COMMENT_#{resub_counter}__"
    resub_counter = resub_counter + 1
    resubs[name] = $1
    "/*#{name}*/"
  }
  
  # find initializations of mocks in field declarations, make them @Mock based
  field_types, field_mock_types = {}, {}
  for_each_method(source) {''}.scan(/.*?[\{\};]/m).each do |statement|
    statement.strip!
#    if (statement =~ /\b(?:final\s+)?((?:public|private|protected)\s+)?(?:final\s+)?((?:org\.jmock\.(?:cglib\.)?)?Mock)\s+(\w+)\s*=\s*mock\s*\(([\w\.]+)\.class\)/)
#      all_match, modifiers, field_dec, field_name, mock_class = $&, $1, $2, $3, $4
#      imports << 'moxie.Mock'
#      source.sub!(Regexp.compile(Regexp.quote(all_match)), "@Mock #{modifiers||' '}#{mock_class} #{field_name} = null")
#      field_types[field_name] = field_mock_types[field_name] = mock_class
#    end
    if (statement =~ /\b((?:(?:private|protected|public)\s+)?(?:org\.jmock\.(?:\w+\.)*)?Mock)\s+(\w+)\s*=\s*mock\s*\(([\w\.]+)\.class\)/)
      all_match, field_dec, field_name, mock_class = $&, $1, $2, $3
      imports << 'moxie.Mock'
      field_dec.sub!(/(^\s*)((?:public|private|protected)\s+)?(?:org\.jmock\.(?:\w+\.)*)?Mock\b/){ "#{$1}@Mock #{$2}#{mock_class}" }
      source.sub!(Regexp.compile(Regexp.quote(all_match)), "#{field_dec} #{field_name} = null")
      field_types[field_name] = field_mock_types[field_name] = mock_class
    end
  end
  
  # complete map of field names/types
  for_each_method(source) {''}.scan(/.*?[\{\};]/m).each do |statement|
    statement.strip!
    if (statement =~ /\b(?:(?:private|protected|public)\s+)?((?:[\w\.>]|<.*?>)+)\s*(\w+)\s*(?:=[^;]+);/m)
      field_types[$2] = $1 if !field_types.has_key?($2)
    end
  end

  # replace all raw JMock 1 calls with raw Moxie calls; translate JUnit 3 to 4
  field_dec_subs = []
  source = for_each_method(source) do |all_match, non_method, method, access, return_type, name, params, body|

    is_constructor = (name =~ /^[A-Z]/ && return_type !~ /\w/)

    if access =~ /\bpublic\b/ && return_type == 'void' && name =~ /^test(\w)(\w*)/
      # looks like a JUnit 3 test method
      new_sig = "@Test\n\tpublic void #{$1.downcase}#{$2}()"
      imports << 'org.junit.Test' if all_match.sub!(Regexp.compile('\bpublic\s+void\s+'+name+'\s*\(\s*\)'), new_sig)
    elsif access =~ /\b(?:protected|public)\b/ && return_type == 'void' && name == 'setUp'
      # JUnit 3 setup
      imports << 'org.junit.Before' if all_match.sub!(/\bprotected\s+void\s+setUp\s*\(\s*\)/m, "@Before\n\tpublic void setUp()")
    elsif access =~ /\b(?:protected|public)\b/ && return_type == 'void' && name == 'tearDown'
      # JUnit 3 teardown
      imports << 'org.junit.After' if all_match.sub!(/\bprotected\s+void\s+tearDown\s*\(\s*\)/m, "@After\n\tpublic void tearDown()")
    end
    
    method_mock_types = {}
    statements = body.scan(/(?:[^\{\};]|\{[^;]*\})*[\{\};]/m)
    statements_orig = statements.collect{|s| s.clone}
    groups = {}
    statements.each_with_index do |st,i|
      st.gsub!(/^([\s\r\n]+)(\bMock\s+)?(\b\w+)\s*=\s*mock\(([\w\.]+)\.class\);/m) do
#      st.gsub!(/^([\s\r\n]+(?:final\s+)?)(\bMock\s+)?(\b\w+)\s*=\s*(?:mock|new\s+Mock)\s*\(([\w\.]+)\.class(?:,\s*"[^"]+")?\);/m) do
        # replace directMock = mock(SomeClass.class)
        method_mock_types[$2 ? "#{name}.#{$3}" : $3] = $4
        if (name == 'setUp' || is_constructor)  && !$2
          # find field declaration and change/annotate as appropriate
          mock_name, mock_class = $3, $4
          field_dec_rx = Regexp.compile('(^\s*)(?:final\s+)?((?:public|private|protected)\s+)?(?:final\s+)?(?:org\.jmock\.(?:cglib\.)?)?Mock\s+'+Regexp.quote(mock_name)+'\b')
          #source.sub!(field_dec_rx) { "#{$1}@Mock #{$2}#{mock_class} #{mock_name}" }
          field_dec_subs << [field_dec_rx, "\\1@Mock \\2#{mock_class} #{mock_name}"]
          imports << 'moxie.Mock'
          ''
        else
          imports << 'moxie.Moxie'
          "#{$1}#{$2 ? ($4+' ') : ''}#{$3} = Moxie.mock(#{$4}.class);"
        end
      end
      st.gsub!(/^(.*?)\b(?:mock|new\s+Mock)\s*\(([\w\.]+)\.class(,\s*"[^"]+")?\)/)  do
        # replace any remaining mock declarations
        if $1.strip[-6,6] == 'Moxie.'
          $&
        else
          imports << 'moxie.Moxie'        
          "#{$1}Moxie.mock(#{$2}.class#{$3})"
        end
      end      
      st.gsub!(/\(([\w\.]+)\)\s*(\w+)\.proxy\(\)/) do
        # replace directMock.proxy()
        "#{$2}"
      end
      st.gsub!(/\.proxy\(\)/, '') # catch any remaining .proxy() calls
      st.sub!(MOCK_EXPECTATION_CALL) do 
        # replace directMock.stubs().method().with(blah).will(doBlah())
        translate_mock_expectation(groups, imports, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
      end
      # intentionally not using org.junit.Assert - it breaks assertEquals(double, double)
      imports << 'junit.framework.Assert' if st.sub!(ASSERT_REGEXP, '\1Assert.\2')  # replace JUnit 3 assertBlah() with JUnit 4 Assert.assertBlah()
    end
    all_match.sub(Regexp.compile(Regexp.quote(statements_orig.join('')))) { statements.join('') }
  end
  field_dec_subs.each {|x| source.sub!(x.first, x.last) }
  
  # miscellaneous tweaks...
  source.sub!(/^import\s+org\.jmock\.Mock\s*;\s*\r?\n?/, '')
  source.sub!(/^import\s+org\.jmock(\.cglib)?\.MockObjectTestCase\s*;\s*\r?\n?/, '')
  if source.sub!(/\bextends\s+(?:MockObject)?TestCase[\s\r\n]*(\bimplements\b.*?)?\{/, '\1{')
    source.sub!(/\bsuper\.setUp\(\);/, '')
    source.sub!(/\bsuper\.tearDown\(\);/, '')
    source.sub!(/(@Override[\s\r\n]+)(@Before[\s\r\n]+public void setUp\(\))/m, '\2')
    source.sub!(/(@Override[\s\r\n]+)(@After[\s\r\n]+public void tearDown\(\))/m, '\2')
  end

  # if a Moxie test, add MoxieRule if not an abstract class
  if imports.detect{|x| x =~ /^moxie\./ }
    source.sub!(/^(\s*public\s+class\s+\w+.*?\{)/) do
      imports << 'moxie.MoxieRule'
      imports << 'org.junit.Rule'
      "#{$1}\n\t@Rule public MoxieRule moxie = new MoxieRule();"
    end
  end

  # strip out empty setUp/tearDown
  source.sub!(/(?:@Override[\s\r\n]+)?(?:public|protected)\s+void\s+setUp\s+\(\)[\s\r\n]+\{(?:[\s\r\n]+super\.setUp\(\);)?[\s\r\n]+\}/m, '')
  source.sub!(/(?:@Override[\s\r\n]+)?(?:public|protected)\s+void\s+tearDown\s+\(\)[\s\r\n]+\{(?:[\s\r\n]+super\.tearDown\(\);)?[\s\r\n]+\}/m, '')

  # add needed imports after last import if not already present
  imports.to_a.sort.each {|i| source = add_import(source, i) }

  resubs.each_pair {|k,v| source.gsub!(k, v) }

  return source  
end

end

#ARGV.each{|arg| source = UnJMock1.convert(File.read(arg)); File.open(arg, 'w') {|f| f.write(source) } } if __FILE__ == $0

#DEBUG: write to stdout instead
ARGV.each{|arg| source = UnJMock1.convert(File.read(arg)); puts "######## #{arg}\n#{source}" } if __FILE__ == $0


__END__


##
##  In general the code above won't parse everything, and won't successfully convert all tests.
##  So you'll want to write your own script to port your codebase over, using the routines above
##  to do most of the grunt work.
##
##  The following is an outline of what the script looked like on my project.  You won't need
##  everything shown below, and will need to add things for your own situation.
##

require 'find'
require "#{__FILE__}/un-jmock1.rb"

# Optionally pass a command line arg to single out one/a few files to convert.
file_pattern = (ARGV[0] =~ /\S/) ? Regexp.compile(ARGV[0]) : /\S/

Find.find(File.dirname(__FILE__)) do |path|

	next if path !~ /Test(?:[BC]ase)?\.java$/
	next if path !~ file_pattern

	# skip "incorrigible" tests that will have to be manually ported
	next if path =~ /(?:
		  ProblematicTest
		| AnotherProblematicTest
		| VeryTightlyCoulpedToJMock1Test
	)\.java$/x

	oldsource = File.read(path)
	next if oldsource =~ /@Test\b/  # try to avoid accidentally converting tests twice
	puts path
	counter = 0
	resubs = {}

	# On certain files, temporarily replace long string constants.
	if path =~ /\/(?: VeryBigStringConstantsTest
			| MoreHugeStringConstantsTest)\.java$/x
		oldsource.scan(/\b(String\s+\w+\s*=[\s\r\n]*)
				(
					(?:"(?:[^"]|\\.)*"|\w+)
					(?:
						[\s\r\n]*\+[\s\r\n]*
						(?:"(?:[^"]|\\.)*"|\w+)
					){3,}
				)([\s\r\n]*;)/x).each do |longstring|
			pre, string, post = longstring
			next if string !~ /[\r\n]/
			name = "__LONG_STRING_#{counter}__"
			counter = counter + 1
			resubs[name] = string
			oldsource.sub!(Regexp.compile(Regexp.quote(pre+string+post)), pre+name+post)
		end
	end

	# All the asserts don't get picked up on certain files, so "pre-assert-ize" them.
	if path =~ /\/(?: OneTest
			| TwoTest
			| RedTest
			| BlueTest)\.java$/x
		oldsource.gsub!(/(^\s*)(assert(?:Equals|True|False|(?:Not)?(?:Null|Same))|fail)\(/, '\1Assert.\2(')
		oldsource.sub!(/(package\s+[\w\.]+;)/,"\\1\n\nimport junit.framework.Assert;")
	end

	# No need to manually verify() your mocks in JUnit 1 when using MockObjectTestCase!
	oldsource.gsub!(/^\s*verify\(\);\s*\n/,'')

	# actually do JMock->Moxie conversion
	newsource = UnJMock1.convert(oldsource)
	resubs.each_pair {|k,v| newsource.gsub!(k, v) }

	# Test-specific tweaks follow.  
	case path
		when /\/BlahTest\.java$/
			# TODO: examples of common postprocessing steps...
			# * you'll need to add type parameters/casts to Moxie.anything() and Moxie.isNull()
			# * ???

	end

	File.open(path,'w') {|f| f.write(newsource) }
end
		
	
